/**
 * resource.js
 * @author Ulyan Sobin
 * @description Cube static resource service module
 */

"use strict";

const fs = require("fs");
const { WebHttpHandler } = require("./cube-web");
const mime = require("./mime");
const view = require("./view");
const { ErrorView } = view;

class StaticResource {

	/** @type {string} */
	path;

	/** @type {object} */
	fstat;

	/** @type {boolean} */
	loaded = false;

	/** @type {Buffer} */
	content = null;

	/** @type {number} */
	weight = 0;

	/**
	 * 
	 * @param {number} fd
	 */
	constructor(path) {
		this.path = path;
	}

	/**
	 * Update the file information
	 * The return value indicates whether the file has been modified.
	 * @returns {boolean}
	 */
	syncStat() {
		let stat = this.fstat;
		this.fstat = fs.statSync(this.path);
		if (stat === undefined) return true;
		return stat.ctimeMs != this.fstat.ctimeMs;
	}

	/**
	 * Load the resource into memory
	 */
	load() {
		this.content = fs.readFileSync(this.path);
		this.loaded = true;
	}

	/**
	 * Unload the resource from memory
	 */
	unload() {
		this.content = null;
		this.loaded = false;
	}

	/**
	 * @returns {Buffer}
	 */
	getContent() {
		this.weight++;
		return this.content;
	}
}


class StaticDirectory extends WebHttpHandler {
	/**
	 * @constant
	 * @type { string }
	 */
	rootPath;

	/**
	 * @constant
	 * @type {Object.<string,StaticResource>}
	 */
	pool;

	/**
	 * @private
	 * @type { number }
	 */
	smallSize = 65536;
	
	/**
	 * @private
	 * @type { number }
	 */
	bufferSize = 1048576; // 1MB
	
	/**
	 * @private
	 * @type { number }
	 */
	totalSize = 0;

	/**
	 * @readonly
	 * @type {?string}
	 */
	default = null;

	/**
	 * 
	 * @param {string} rootPath 
	 * @param {object} options
	 * @param {number} options.smallSize
	 * @param {number} options.bufferSize
	 * @param {string} options.default
	 */
	constructor (rootPath, options) {
		super((client) => {
			let rpath = client.path;
			let res;
			let divs;
			try {
				if ((rpath == "/" || rpath == "") && typeof(this.default) == "string") {
					res = this.get(this.default);
					divs = this.default.split('.');
				}
				else {
					res = this.get(rpath);
					divs = rpath.split('.');
				}
			} catch (err) {
				// 404
				new ErrorView(404, "NOT FOUND").publish(client);
				return;
			}
			let exname = divs[divs.length - 1];
			let mime_type = mime.getType(exname);
			if (res instanceof Buffer) {
				client.response.press(200, {
					"Content-Type": mime_type,
					"Content-Length": res.length
				}, res);
			}
			else if (res && res.stream instanceof fs.ReadStream) {
				client.response.from(200, {
					"Content-Type": mime_type,
					"Content-Length": res.size
				}, res.stream);
			}
			else {
				throw new Error("Unknown type of `res`");
			}
		});
		this.rootPath = rootPath;
		this.pool = {};
		if (options) {
			if (options.smallSize) this.smallSize = options.smallSize;
			if (options.bufferSize) this.bufferSize = options.bufferSize;
			if (typeof(options.default) == "string") {
				this.default = options.default;
			}
			else if (options.default instanceof Array) {
				for (let fp of options.default) {
					const ap = `${this.rootPath}${fp}`;
					if (fs.statSync(ap)) {
						this.default = ap;
						break;
					}
				}
			}
		}
	}

	/**
	 * @private
	 * @param {string} path
	 * @throws {Error}
	 * @returns {Buffer|{
	 *   stream: fs.ReadStream,
	 *   size: number
	 * }}
	 */
	get(path) {
		let src = this.pool[path];
		if (!(src instanceof StaticResource)) {
			// resource not in the pool
			src = new StaticResource(`${this.rootPath}/${path}`);
			src.syncStat();
			if (!src.fstat.isFile()) throw new Error("Resource must be file");
			this.pool[path] = src;
		}
		let orisize = src.loaded ? src.fstat.size : 0;
		let changed = src.syncStat();
		if (src.loaded && !changed) {
			return src.getContent();
		}
		else {
			if (src.fstat.size <= this.smallSize) {
				// can be load
				let newsize = this.totalSize - orisize + src.fstat.size;
				if (newsize <= this.bufferSize) {
					// load
					src.load();
					this.totalSize = newsize;
					return src.getContent();
				}
				else {
					// remove something
					let aim = null;
					let minw = Number.MAX_VALUE;
					for (let p in this.pool) {
						let s = this.pool[p];
						if (s.weight < minw && s.content.length >= src.fstat.size) {
							// mark for reload
							aim = s;
							minw = s.weight;
						}
					}
					if (aim === null)
						return fs.readFileSync(src.path);
					else {
						aim.unload();
						src.load();
						return src.getContent();
					}
				}
			}
			else {
				// too large, return stream
				return {
					stream: fs.createReadStream(src.path),
					size: src.fstat.size
				};
			}
		}
	}
}

class StaticFile extends WebHttpHandler {

	/**
	 * @private
	 * @type { Buffer|fs.PathLike }
	 */
	source;

	/**
	 * 
	 * @param {fs.PathLike} path
	 */
	constructor(path) {
		super((client) => {
			let head = {
				"Content-Type": mime.mimeOf(path),
			};
			try {
				if (this.source instanceof Buffer) {
					client.response.press(200, head, this.source);
				}
				else if (this.source instanceof fs.PathLike) {
					client.response.from(200, head, fs.createReadStream(this.source));
				}
				else {
					throw new Error();
				}
			} catch (err) {
				new ErrorView(404, "NOT FOUND").publish(client);
			}
		});
		let stat = fs.statSync(path);
		if (stat.size < 1048576) {
			this.source = fs.readFileSync(path);
		}
		else {
			this.source = path;
		}
	}
}

module.exports.StaticDirectory = StaticDirectory;
module.exports.StaticFile = StaticFile;